package nf;

import java.util.*;
import javax.swing.*;
import java.awt.*;
import nf.graph.Graph;
import nf.graph.GraphEventListener;
import nf.graph.UndirectedGraph;
import nf.graph.Delta;
import nf.graph.Node;
import nf.graph.Edge;
import nf.util.Rational;
import nf.utility.UtilityCalculator;
import java.awt.event.*;
import java.io.File;
import java.io.IOException;

public class GraphDisplay extends JLabel
{
	private Graph graph;
	private Repainter repainter;
	private volatile boolean painted;
	private volatile boolean animationPending;
	
	private int[][] coords;
	private double[][] vels;
	
	private int[][] nodeDimensions;
	
	private Node[] nodes;
	private double[] decimalUtilities;
	private double maxUtility;
	private Map<Node,Rational> utils;
	private UtilityCalculator utilCalc;
	private boolean completed;
	private boolean showDensityDisplay;

	private boolean nodeClicked;
	private int clickedNode;
	private int[] oldMousePosition = new int[2];
	
	private Set<AnimationListener> animationListeners;
	
	private FontMetrics metrics;
	private static final float BOX_OPACITY = 0.75f;
	private static final int BOX_PADDING = 5 /* pixels */;
	private static final Color DENSITY_BG = new Color(24, 30, 38);
	private static final Color STABLE_BG = new Color(16, 127, 201);
	private static final Color ANIMATING_BG = new Color(135, 150, 15);
	
	public GraphDisplay(Graph graph)
	{
		this(graph, null);
	}
	
	public GraphDisplay(Graph graph, UtilityCalculator utilCalc)
	{
		this.graph = graph;
		repainter = new Repainter();
		graph.addEventListener(repainter);
		int n = graph.size();
		
		this.utilCalc = utilCalc;
		if (utilCalc != null)
			decimalUtilities = new double[n];
		maxUtility = 0.0;
		
		completed = false;
		painted = false;
		animationPending = false;
		
		animationListeners = new HashSet<AnimationListener>();
		
		addMouseListener(new MouseAdapter() {
			public void mouseClicked(MouseEvent e) {
				if (e.getButton() == e.BUTTON3)
					runPhysics();
			}
			public void mouseReleased(MouseEvent e) {
				nodeClicked = false;
			}
			public void mousePressed(MouseEvent e) {
				if (e.getButton() == e.BUTTON1) {
					//find the closest node
					int closestNode = -1;
					int n = nodes.length;
					
					int m_x = e.getX() - getWidth()/2;
					int m_y = e.getY() - getHeight()/2;
					
					for (int i = 0; i < n; i++) {
						int i_x = coords[i][0];
						int i_y = coords[i][1];
						
						int nodeWidth = nodeDimensions[i][0];
						int nodeHeight = nodeDimensions[i][1];
						
						i_x -= nodeWidth / 2;
						i_y -= nodeHeight / 2;
						int i_x2 = i_x + nodeWidth;
						int i_y2 = i_y + nodeHeight;
						if (m_x > i_x && m_x < i_x2) {
							if (m_y > i_y && m_y < i_y2) {
								closestNode = i;
								break;
							}
						}
					}
					if (closestNode != -1) {
						nodeClicked = true;
						clickedNode = closestNode;
						oldMousePosition[0] = e.getX();
						oldMousePosition[1] = e.getY();
					}
				}
			}
		});
		
		addMouseMotionListener(new MouseMotionAdapter() {
			public void mouseDragged(MouseEvent e) {
				if (nodeClicked) {
					int newMouseX = e.getX();
					int newMouseY = e.getY();
					
					int deltaX = newMouseX - oldMousePosition[0];
					int deltaY = newMouseY - oldMousePosition[1];
					
					coords[clickedNode][0] += deltaX;
					coords[clickedNode][1] += deltaY;
					
					oldMousePosition[0] = newMouseX;
					oldMousePosition[1] = newMouseY;

					repaint();
				}
			}
		});
		showDensityDisplay = false;
	}
	
	public void addNotify()
	{
		super.addNotify();
		graph.addEventListener(repainter);
	}
	
	public void removeNotify()
	{
		super.removeNotify();
		graph.removeEventListener(repainter);
	}
	
	public boolean addAnimationListener(AnimationListener l)
	{
		return animationListeners.add(l);
	}
	
	public boolean removeAnimationListener(AnimationListener l)
	{
		return animationListeners.remove(l);
	}
	
	public static interface AnimationListener
	{
		public void animationDone();
	}
	
	private class Repainter implements GraphEventListener
	{
		public void nodeAdded(Graph graph, Node node)
		{
			repaint();
		}
		
		public void nodeRemoved(Graph graph, Node node)
		{
			repaint();
		}
		
		public void edgeAdded(Graph graph, Edge edge)
		{
			repaint();
		}
		
		public void edgeRemoved(Graph graph, Edge edge)
		{
			repaint();
		}
	}
	
	public void setCompleted()
	{
		completed = true;
		repaint();
	}
	
	public void showAverageDegree(boolean show)
	{
		showDensityDisplay = show;
		repaint();
	}
	
	private void calculate()
	{
		int n = graph.size();
		int nMin = Integer.MAX_VALUE;
		
		if (nodes == null) {
			nodes = new Node[n];
			coords = new int[n][2];
			vels = new double[n][2];
			nodeDimensions = new int[n][2];

			double r = Math.min(getHeight(), getWidth()) / 3 - 16;
			double theta = Math.PI*2 / (double)n;
			double pos = 0;
			for(int c=0; c<n; c++){
				coords[c][0] = (int)(r * Math.cos(pos));
				coords[c][1] = (int)(r * Math.sin(pos));
				pos+=theta;
			}
			
			for(Node m : graph) {
				try {
					int nTemp = Integer.parseInt(m.getName());
					if (nTemp < nMin)
						nMin = nTemp;
					} catch (NumberFormatException e) {
						throw new RuntimeException(e);
				}
			}

			for (Node m : graph) {
				try {
					nodes[Integer.parseInt(m.getName()) - nMin] = m;
				} catch (NumberFormatException e) {
					throw new RuntimeException(e);
				}
			}
			
			int padding = 2;
			int edgeRadius = 4;
			for (int c = 0; c < n; c++) {
				String label = nodes[c].getName();
			
				if (label == null)
					label = "?";
				int textWidth = metrics.stringWidth(label);
				int textHeight = metrics.getHeight();
				nodeDimensions[c][0] = textWidth + padding * 2;
				nodeDimensions[c][1] = textHeight + padding * 2;
			}
		}
		
		if (utilCalc == null)
			return;
		
		utils = utilCalc.getUtilities(graph);
		
		maxUtility = 0.0;
		for (int i = 0; i < nodes.length; i++) {
			decimalUtilities[i] = (double)
				((int) (utils.get(nodes[i]).doubleValue() * 100.0) / 100.0);
			if (decimalUtilities[i] > maxUtility)
				maxUtility = decimalUtilities[i];
		}
	}
	
	private Color getNodeColor(double utility)
	{
		if (utilCalc == null) {
			return new Color(255, 0, 0, 225);
		}
		
		int blend = 40;
		if (maxUtility > 0)
			blend = (int) Math.max(40.0, (utility / maxUtility) * 255.0);
		return new Color(255, 0, 0, blend);
	}
	
	private Color changeColorOpacity(Color color, float newOpacity)
	{
		return new Color(color.getColorSpace(), color.getComponents(null),
			newOpacity);
	}
	
	private void drawBox(Graphics g, String label, int x, int y,
		Color back, Color fore)
	{
		if (metrics == null) {
			metrics = g.getFontMetrics();
		}
		
		int ascent = metrics.getAscent();
		int descent = metrics.getDescent();
		int textHeight = metrics.getHeight();
		int textWidth = metrics.stringWidth(label);
		int doublePadding = BOX_PADDING * 2;
		int boxWidth = textWidth + doublePadding;
		int boxHeight = textHeight + doublePadding;
		
		if (x < 0) {
			x = getWidth() - boxWidth + x;
		}
		if (y < 0) {
			y = getHeight() - boxHeight + y;
		}
		
		g.setColor(changeColorOpacity(back, BOX_OPACITY));
		g.fillRoundRect(x, y, boxWidth, boxHeight, 6, 6);
		
		g.setColor(changeColorOpacity(fore, BOX_OPACITY));
		g.drawString(label, x + BOX_PADDING, y + BOX_PADDING + ascent);
	}
	
	public void paint(Graphics g)
	{
		Graphics2D g2d = (Graphics2D) g;
		metrics = g.getFontMetrics();
		int ascent = metrics.getAscent();
		int descent = metrics.getDescent();
		int textHeight = metrics.getHeight();
		
		calculate();
		
		g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
			RenderingHints.VALUE_ANTIALIAS_ON);
		
		int width = getWidth();
		int height = getHeight();
		int centerX = width / 2;
		int centerY = this.getHeight() / 2;
		//g.fillRect(centerX,centerY,2,2);

		for (int c = 0; c < coords.length; c++) {
			g.setColor(Color.GRAY);
			for (int d = c+1; d < coords.length; d++) {
				if (graph.connected(nodes[c],nodes[d])) {
					g.drawLine(centerX+coords[c][0],centerY+coords[c][1],
						centerX+coords[d][0],centerY+coords[d][1]);
				}
			}
		}
		
		final int padding = 2;
		final int edgeRadius = 4;
		for (int c = 0; c < coords.length; c++) {
			Rational value = (utils.get(nodes[c]));
			double valueD = decimalUtilities[c];
			String label = nodes[c].getName();
			Color nodeColor;
			
			if (label == null)
				label = "?";
			int textWidth = metrics.stringWidth(label);
			
			int x = centerX + coords[c][0];
			int y = centerY + coords[c][1];
			
			int nodeWidth = nodeDimensions[c][0];
			int nodeHeight = nodeDimensions[c][1];
			
			g.setColor(Color.DARK_GRAY);
			g.drawRoundRect(x - (nodeWidth / 2), y - (nodeHeight / 2),
				nodeWidth, nodeHeight, edgeRadius, edgeRadius);
			
			nodeColor = getNodeColor(valueD);
			g.setColor(nodeColor);
			g.fillRoundRect(x - (nodeWidth / 2) + 1, y - (nodeHeight / 2) + 1,
				nodeWidth, nodeHeight, edgeRadius, edgeRadius);
			
			if (nodeColor.getAlpha() >= 90)	
				g.setColor(Color.WHITE);
			else
				g.setColor(Color.DARK_GRAY);
			g.drawString(label, x - (nodeWidth / 2) + padding,
				y - (nodeHeight / 2) + padding + ascent);
			
			g.setColor(Color.RED);
			int textX = x;
			int textY = y;
			label = value + " = " + valueD;
			textWidth = metrics.stringWidth(label);
			
			if (textX + textWidth - 7 > width - 6)
				textX -= textWidth;
			else
				textX -= 7;
			
			if (textY + textHeight + 7 > height - 6)
				textY -= 7 + descent;
			else
				textY += 7 + ascent;
			
			g.drawString(label, textX, textY);
		}
		
		if (completed) {
			drawBox(g, "Equilibrium", 7, 7, STABLE_BG, Color.WHITE);
		}
		
		if (animating) {
			drawBox(g, "Animating...", -7, 7, ANIMATING_BG, Color.WHITE);
		}
		
		if (showDensityDisplay) {
			String label = String.format("Density: %.4f",
				graph.getAverageDegree());
			drawBox(g, label, 7, -7, DENSITY_BG, Color.WHITE);
		}
		
		painted = true;
	}
	
	
	double temperature = 10;
	final double start_temp = 10;
	double k;
	
	private double f_rep(double distance)
	{
		return k * k / distance;
	}
	
	private double f_attr(double distance)
	{
		return distance * distance / k;
	}
	
	public void runPhysicsUntilDone()
	{
		while (temperature > .1) {
			runPhysics();
		}
		temperature = start_temp;
	}
	
	javax.swing.Timer timer;
	boolean animating = false;
	public void animatePhysics()
	{
		if (!animating) {
			int delay = 1000 / 30;
			ActionListener animator = new ActionListener() {
				public void actionPerformed(ActionEvent e)
				{
					runPhysics();
					repaint();
					
					if (temperature < .1) {
						temperature = start_temp;
						animating = false;
						timer.stop();
						for (AnimationListener l : animationListeners) {
							l.animationDone();
						}
						
					}
				}
			};
			timer = new javax.swing.Timer(delay, animator);
			animating = true;
			timer.start();
		}
	}
	
	public void runPhysics()
	{
		if (!painted)
			return;
		
		int n = nodes.length;
		
		int width = getWidth() - 100;
		int height = getHeight() - 100;
		double area = (width) * (height);
		k = Math.sqrt(area / (double)(n));

		for (int i = 0; i < n; i++) {
			vels[i][0] = 0;
			vels[i][1] = 0;
		}
		
		for (int i = 0; i < n; i++) {
			for (int j = i + 1; j < n; j++) {
				Node a = nodes[i];
				Node b = nodes[j];
				
				int a_x = coords[i][0];
				int a_y = coords[i][1];
				int b_x = coords[j][0];
				int b_y = coords[j][1];
				
				//calculate b - a
				double ab_x = b_x - a_x;
				double ab_y = b_y - a_y;
				
				//if connected, natural distance is 100
				//else, natural distance is 300
					double distance = Math.sqrt(ab_x * ab_x + ab_y * ab_y);
				if (distance < .01) distance = .01; //no 0 allowed
				
				//Repulsive Forces:
				
				//for b
				double fb_x = (ab_x / distance) * f_rep(distance);
				double fb_y = (ab_y / distance) * f_rep(distance);
				
				//for a
				double fa_x = -fb_x;
				double fa_y = -fb_y;
				
				vels[i][0] += fa_x;
				vels[i][1] += fa_y;
				
				vels[j][0] += fb_x;
				vels[j][1] += fb_y;
				
				//Attractive Forces:
				
				boolean connected = graph.connected(a, b);
				
				if (connected) {
					//for b
					fb_x = -(ab_x / distance) * f_attr(distance);
					fb_y = -(ab_y / distance) * f_attr(distance);
					
					//for a
					fa_x = -fb_x;
					fa_y = -fb_y;
					
					vels[i][0] += fa_x;
					vels[i][1] += fa_y;
				
					vels[j][0] += fb_x;
					vels[j][1] += fb_y;
				}
			}
		}
		
		for (int i = 0; i < n; i++) {			
			double vel_x = vels[i][0];
			double vel_y = vels[i][1];
			
			double speed = Math.sqrt(vel_x * vel_x + vel_y * vel_y);
				//normalize
			vel_x = vel_x / speed;
			vel_y = vel_y / speed;
				//temperature limit
			if (temperature < speed)
				speed = temperature;
				coords[i][0] += vel_x * speed;
			coords[i][1] += vel_y * speed;
			
			int W2 = (width) / 2;
			int pos_x = (-W2 > coords[i][0] ? -W2 : coords[i][0]);
			pos_x = (W2 < pos_x ? W2 : pos_x);
			int L2 = (height) / 2;
			int pos_y = (-L2 > coords[i][1] ? -L2 : coords[i][1]);
			pos_y = (L2 < pos_y ? L2 : pos_y);
			
			coords[i][0] = pos_x;
			coords[i][1] = pos_y;
		}
		
		temperature = temperature * .95;
	}
	
	public void centerCoords()
	{
		double center_x = 0;
		double center_y = 0;
	
		int n = nodes.length;
	
		for (int i = 0; i < n; i++) {
			center_x += coords[i][0];
			center_y += coords[i][1];
		}
		center_x = center_x / (double)n;
		center_y = center_y / (double)n;
		for (int i = 0; i < n; i++) {
			coords[i][0] -= center_x;
			coords[i][1] -= center_y;
		}
	}
}
